package log

import (
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

const DefaultMaxSize = 100 * 1024 * 1024 //100M
const FilenameLayout = "20060102150405"

// 按时间分割日志
const (
	CUTDUR_M   time.Duration = time.Minute
	CUTDUR_M5                = time.Minute * 5
	CUTDUR_M10               = time.Minute * 10
	CUTDUR_M15               = time.Minute * 15
	CUTDUR_M20               = time.Minute * 20
	CUTDUR_M30               = time.Minute * 30
	CUTDUR_H                 = time.Hour
	CUTDUR_H2                = time.Hour * 2
	CUTDUR_H3                = time.Hour * 3
	CUTDUR_H4                = time.Hour * 4
	CUTDUR_H6                = time.Hour * 6
	CUTDUR_H8                = time.Hour * 8
	CUTDUR_H12               = time.Hour * 12
	//以下遇每月1号分割
	CUTDUR_D  = time.Hour * 24 //每1天分割，取值范围[1D,2D)
	CUTDUR_D2 = CUTDUR_D * 2   //每2天分割，取值范围[2D,3D)
	CUTDUR_D3 = CUTDUR_D * 3   //每3天分割，取值范围[3D,4D)
	CUTDUR_D4 = CUTDUR_D * 4   //每4天分割，取值范围[4D,5D)
	CUTDUR_D5 = CUTDUR_D * 5   //每5天分割，取值范围[5D,6D)
	//
	CUTDUR_D6  = 1              //按周分割，从日志开始记录按周分割，取值范围[6D,7D)
	CUTDUR_D7  = CUTDUR_D * 7   //按周分割，每周一进行分割，取值范围[7D,8D)
	CUTDUR_D8  = 2              //按周分割，每周六进行分割，取值范围[8D,9D)
	CUTDUR_D9  = 3              //按周分割，每周天进行分割，取值范围[9D,10D)
	CUTDUR_D10 = CUTDUR_D * 10  //按旬分割，每月1号11号21号进行分割，取值范围[10D,15D)
	CUTDUR_D15 = CUTDUR_D * 15  //按半月分割，每月1号16号进行分割，取值范围[16D,28D)
	CUTDUR_D30 = CUTDUR_D * 30  //按月分割，取值范围[28D,90D)
	CUTDUR_Q   = CUTDUR_D30 * 3 //按季分割，取值范围[90D,180D)
	CUTDUR_Q2  = CUTDUR_Q * 2   //按半年分割，取整范围[180D,360D)
	CUTDUR_Y   = CUTDUR_Q * 4   //按年分割，取整范围[360D,+∞)
)

// 文件切换通知函数，参数是已经完成的文件名
type FileCut func(string)

type Filer struct {
	m        sync.Mutex
	prefex   string //日志文件前缀
	file     *os.File
	stop     chan struct{}
	path     string //文件保存根目录
	curPath  string //当前文件保存目录
	ext      string //日志文件后缀,默认".log"
	filename string //当前文件全名
	cutSize  int    //文件分割最大容量，0不切割，默认为DefaultMaxSize,最小64KB
	//按时间分割文件，小于等于0不分割
	//最小时间单位为1分钟，低于1分钟的按1分钟切割
	//低于30分钟的按分钟切割：按整数倍分钟切割，向上取整
	//低于12小时的，按30分钟整分割：按30分钟向下取整
	//低于1天的，按小时向下取整。如果cutSize被设置，低于1天的将变成1天
	//详细见CUTDUR预定义
	//目录创建规则：每年一个一级目录，小于月的每月一个二级目录，小于天的分割每天一个三级目录
	dur      time.Duration //文件夹分割天数,大于年的天数按年存储[365-],大于月天数按月存储[28-]
	saveDays int           //文件最多保存天数,大于0有效
	delPoint int           //循环删除时间节点

	fileCut FileCut //文件切换通知，可用于自定义时间切割与循环删除

	wsize       int        //当前写入大小
	month_count int        //每月文件生成数
	month       time.Month //写在当前的月

	//fileCreateTime time.Time  //文件创建时间
	nextTime    time.Time   //下一次切换节点时间
	rotateTimer *time.Timer //循环删除定时器
}

type FilerOption func(*Filer)

func FilePrefex(p string) FilerOption {
	return func(f *Filer) {
		if len(p) == 0 {
			f.prefex = ""
		} else {
			f.prefex = p
		}
	}
}

func FileRoot(p string) FilerOption {
	return func(f *Filer) {
		if len(p) == 0 {
			f.path = "./"
		} else {
			f.path = p
		}
	}
}

func FileExt(ext string) FilerOption {
	return func(f *Filer) {
		if len(ext) > 0 {
			p := strings.IndexRune(ext, '.')
			if p >= 0 {
				f.path = ext[p:]
			} else {
				f.path = "." + ext
			}
		}
	}
}

// 日志文件最大字节数
func FileMaxSize(m int) FilerOption {
	return func(f *Filer) {
		if m > 0 && m < 64000 {
			m = 64000
		}
		if m > 0x40000000 {
			m = 0x40000000
		}
		f.cutSize = m
	}
}

// 日志最多保存时长
func FileSaveDays(m int) FilerOption {
	return func(f *Filer) {
		if m > 36000 {
			f.saveDays = 36000 //要防止duation溢出
		} else {
			f.saveDays = m
		}
	}
}

// 循环删除时间节点，参数未时分秒
func FileDelete(h, m, s int) FilerOption {
	return func(f *Filer) {
		if h < 0 || h > 23 {
			h = 0
		}
		if m < 0 || m > 59 {
			m = 0
		}
		if s < 0 || s > 59 {
			s = 0
		}
		f.delPoint = h*10000 + m*100 + s
	}
}

// 按时间分割文件
func FileDuration(d time.Duration) FilerOption {
	return func(f *Filer) {
		f.dur = parseFileDuration(d)
	}
}

// 文件切换通知
func FWFileCut(fc FileCut) FilerOption {
	return func(f *Filer) {
		f.fileCut = fc
	}
}

func NewFiler(opts ...FilerOption) (*Filer, error) {
	f := &Filer{
		path:     "log",
		ext:      ".log",
		cutSize:  DefaultMaxSize,
		dur:      CUTDUR_D,
		saveDays: 0,
		delPoint: 23000,
	}
	for _, opt := range opts {
		opt(f)
	}
	if f.fileCut == nil {
		f.fileCut = func(string) {}
	}
	if f.cutSize > 0 && f.dur > 0 && f.dur < CUTDUR_D {
		f.dur = CUTDUR_D //cutSize被设置，最低1天变更一次
	}
	t := time.Now()
	//设置切换文件时间
	if f.dur < CUTDUR_M {
		f.nextTime = time.Date(9999, 12, 31, 23, 59, 59, 0, t.Location())
	} else {
		f.nextTime = t.Add(time.Millisecond * -1)
		f.setNextTime(t)
	}
	//创建三级目录
	f.createDirs(t)
	//创建日志文件
	var err error
	f.file, f.filename, err = f.openFile(t)
	if err != nil {
		return nil, err
	}
	//初始化stop通道
	f.stop = make(chan struct{})
	//启动循环删除功能
	f.rotate(t)
	return f, nil
}

func (f *Filer) Close() error {
	select {
	case <-f.stop:
		return os.ErrClosed
	default:
		f.m.Lock()
		defer f.m.Unlock()
		select {
		case <-f.stop:
			return os.ErrClosed
		default:
			close(f.stop) //关闭通道
			f.cutSize = 0 //关闭文件切换
			f.nextTime = time.Date(9999, time.December, 31, 23, 59, 59, 999, time.Local)
			return f.file.Close() //关闭文件
		}
	}
}

func (f *Filer) Write(content []byte) (int, error) {
	t := time.Now()
	_ = f.cutCheck(t)
	s, err := f.file.Write(content)
	if s > 0 {
		f.wsize += s
	}
	return s, err
}

// 判断文件大小是否超标
func (f *Filer) sizeup() bool {
	return f.cutSize > 0 && f.wsize > f.cutSize
}

// 检测文件是否切换
func (f *Filer) cutCheck(t time.Time) error {
	if f.sizeup() || t.After(f.nextTime) {
		select {
		case <-f.stop:
			return os.ErrClosed
		default:
			err := f.createFile(t)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (f *Filer) openFile(t time.Time) (*os.File, string, error) {
	fn := t.Format(FilenameLayout) + f.ext
	if f.prefex != "" {
		fn = f.prefex + "_" + fn
	}
	fn = filepath.Join(f.curPath, fn)
	file, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	return file, fn, err
}

// 创建日志文件并通知已完成的文件
func (f *Filer) createFile(t time.Time) error {
	//创建目录
	f.createDirs(t)
	//创建文件
	fp, fn, err := f.openFile(t)
	if err != nil {
		return err
	}
	//旧文件
	oldfp := f.file
	oldname := f.filename
	//切换文件
	f.file = fp
	f.filename = fn
	f.wsize = 0
	f.setNextTime(t)
	f.month_count++
	//关闭旧文件
	oldfp.Close()
	//通知文件切换
	go func(filename string) {
		f.fileCut(filename)
	}(oldname)
	return nil
}

func (f *Filer) createDirs(t time.Time) error {
	var path string
	y, m, _ := t.Date()
	mm := wid2(int(m))
	path = filepath.Join(f.path, strconv.Itoa(y))
	if f.month != m {
		month_path := filepath.Join(f.curPath, mm)
		mfile := strconv.Itoa(y) + mm
		matches, er := filepath.Glob(f.curPath + "/*" + f.ext)
		if er == nil {
			if len(matches) > 0 {
				os.MkdirAll(month_path, os.ModePerm)
			}
			ct := 0
			for _, match := range matches {
				name := filepath.Base(match)
				if strings.HasPrefix(name, mfile) {
					os.Rename(match, filepath.Join(month_path, name))
					ct++
				}
			}
			if ct == 0 {
				os.Remove(month_path)
			}
		}
		f.month = m
		f.month_count = 0
	}
	if path != f.curPath {
		f.curPath = path
		err := os.MkdirAll(f.curPath, os.ModePerm)
		return err
	}
	return nil
}

func (f *Filer) rotate(t time.Time) {
	if f.saveDays > 0 && f.rotateTimer == nil {
		maxDur := CUTDUR_D * time.Duration(f.saveDays)
		y, m, d := t.Date()
		h := f.delPoint / 10000
		mm := (f.delPoint % 10000) / 100
		s := f.delPoint % 100
		nt := time.Date(y, m, d, h, mm, s, 0, t.Location())
		nt = nt.Add(maxDur)
		f.rotateTimer = time.NewTimer(nt.Sub(t))
		//启动计时器
		go func() {
		Timer:
			for {
				select {
				case <-f.stop:
					break Timer
				case <-f.rotateTimer.C:
				}
				//下次循环删除时间
				t := time.Now()
				y, m, d := t.Date()
				h := f.delPoint / 10000
				mm := (f.delPoint % 10000) / 100
				s := f.delPoint % 100
				nt := time.Date(y, m, d, h, mm, s, 0, t.Location())
				nt = nt.Add(maxDur)
				//set next timepoint
				f.rotateTimer.Reset(nt.Sub(t))
				//遍历文件夹
				_ = filepath.WalkDir(f.path, func(path string, d fs.DirEntry, err error) error {
					if err == nil {
						if d.IsDir() {
							//删除空文件夹
							_ = os.Remove(path)
						} else {
							name := d.Name()
							if len(name) > 14 && filepath.Ext(name) == f.ext {
								o, err := time.Parse(FilenameLayout, name[:14])
								if err == nil && t.Sub(o) > maxDur {
									//判断文件修改时间是否超时
									info, err := d.Info()
									if err == nil && t.Sub(info.ModTime()) > maxDur {
										//删除文件
										_ = os.Remove(path)
									}
								}
							}
						}
					}
					return nil
				})
			}
			_ = f.rotateTimer.Stop()
			f.rotateTimer = nil
		}()
	}
}

// 设置下一次文件切换时间
func (f *Filer) setNextTime(t time.Time) {
	if t.After(f.nextTime) {
		//使用t为锚定时间，防止修改了系统时间
		oy, om, od := t.Date()
		switch f.dur {
		case CUTDUR_Y: //年
			f.nextTime = time.Date(oy+1, time.January, 1, 0, 0, 0, 0, t.Location())
		case CUTDUR_Q2: //半年
			if om > time.January {
				f.nextTime = time.Date(oy+1, time.January, 1, 0, 0, 0, 0, t.Location())
			} else {
				f.nextTime = time.Date(oy, time.July, 1, 0, 0, 0, 0, t.Location())
			}
		case CUTDUR_Q: //季
			if om < time.April {
				f.nextTime = time.Date(oy, time.April, 1, 0, 0, 0, 0, t.Location())
			} else if om < time.July {
				f.nextTime = time.Date(oy, time.July, 1, 0, 0, 0, 0, t.Location())
			} else if om < time.October {
				f.nextTime = time.Date(oy, time.October, 1, 0, 0, 0, 0, t.Location())
			} else {
				f.nextTime = time.Date(oy+1, time.January, 1, 0, 0, 0, 0, t.Location())
			}
		case CUTDUR_D30: //月
			if om == time.December {
				f.nextTime = time.Date(oy+1, time.January, 1, 0, 0, 0, 0, t.Location())
			} else {
				f.nextTime = time.Date(oy, om+1, 1, 0, 0, 0, 0, t.Location())
			}
		case CUTDUR_D15: //半月
			if od > 1 {
				om++
				od = 1
				if om > time.December {
					om = time.January
					oy++
				}
			} else {
				od = 16
			}
			f.nextTime = time.Date(oy, om, od, 0, 0, 0, 0, t.Location())
		case CUTDUR_D10: //每旬
			if od > 20 {
				om++
				od = 1
				if om > time.December {
					om = time.January
					oy++
				}
			} else if od > 10 {
				od = 21
			} else {
				od = 11
			}
			f.nextTime = time.Date(oy, om, od, 0, 0, 0, 0, t.Location())
		case CUTDUR_D9: //按周分割，每周天进行分割
			f.setWeekday(t, time.Sunday)
		case CUTDUR_D8: //按周分割，每周六进行分割
			f.setWeekday(t, time.Saturday)
		case CUTDUR_D7: //按周分割，每周一进行分割
			f.setWeekday(t, time.Monday)
		case CUTDUR_D6: //按周分割
			t = t.Add(CUTDUR_D7)
			h, m, d := t.Date()
			f.nextTime = time.Date(h, m, d, 0, 0, 0, 0, t.Location())
		case CUTDUR_D5:
			f.nextTime_addDay(oy, om, od, 5, t.Location())
		case CUTDUR_D4:
			f.nextTime_addDay(oy, om, od, 4, t.Location())
		case CUTDUR_D3:
			f.nextTime_addDay(oy, om, od, 3, t.Location())
		case CUTDUR_D2:
			f.nextTime_addDay(oy, om, od, 2, t.Location())
		case CUTDUR_D:
			t = t.Add(CUTDUR_D)
			h, m, d := t.Date()
			f.nextTime = time.Date(h, m, d, 0, 0, 0, 0, t.Location())
		default:
			if f.dur >= CUTDUR_M {
				t = t.Add(f.dur)
				h, m, d := t.Date()
				f.nextTime = time.Date(h, m, d, t.Hour(), t.Minute(), 0, 0, t.Location())
			}
		}
	}
}

func (f *Filer) setWeekday(t time.Time, wd time.Weekday) {
	t = t.Add(CUTDUR_D)
	for i := 0; i < 7; i++ {
		if t.Weekday() != wd {
			t = t.Add(CUTDUR_D)
		}
	}
	h, m, d := t.Date()
	f.nextTime = time.Date(h, m, d, 0, 0, 0, 0, t.Location())
}

func (f *Filer) nextTime_addDay(y int, m time.Month, d int, days int, loc *time.Location) {
	if d < (29 - days) {
		d = (d-1)/days*days + days + 1
	} else {
		d = 1
		m++
		if m > time.December {
			m = time.January
			y++
		}
	}
	f.nextTime = time.Date(y, m, d, 0, 0, 0, 0, loc)
}

func wid2(t int) string {
	r := "0" + strconv.Itoa(t)
	return r[len(r)-2:]
}

func parseFileDuration(d time.Duration) time.Duration {
	if d <= 0 {
		return 0
	}
	if d >= CUTDUR_Y {
		return CUTDUR_Y
	}
	if d >= CUTDUR_Q2 {
		return CUTDUR_Q2
	}
	if d >= CUTDUR_Q {
		return CUTDUR_Q
	}
	if d >= CUTDUR_D30 {
		return CUTDUR_D30
	}
	if d >= CUTDUR_D15 {
		return CUTDUR_D15
	}
	if d >= CUTDUR_D10 {
		return CUTDUR_D10
	}
	if d >= CUTDUR_D*9 || d == CUTDUR_D9 {
		return CUTDUR_D9
	}
	if d >= CUTDUR_D*8 || d == CUTDUR_D8 {
		return CUTDUR_D8
	}
	if d >= CUTDUR_D7 {
		return CUTDUR_D7
	}
	if d >= CUTDUR_D*6 || d == CUTDUR_D6 {
		return CUTDUR_D6
	}
	if d >= CUTDUR_D5 {
		return CUTDUR_D5
	}
	if d >= CUTDUR_D4 {
		return CUTDUR_D4
	}
	if d >= CUTDUR_D3 {
		return CUTDUR_D3
	}
	if d >= CUTDUR_D2 {
		return CUTDUR_D2
	}
	if d >= CUTDUR_D {
		return CUTDUR_D
	}
	if d >= CUTDUR_H12 {
		return d / CUTDUR_H * CUTDUR_H
	}
	if d >= CUTDUR_M30 {
		return d / CUTDUR_M30 * CUTDUR_M30
	}
	if d > CUTDUR_M {
		return (d + time.Second*59) / CUTDUR_M * CUTDUR_M
	}
	return CUTDUR_M
}
